// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import "package:expect/async_helper.dart";
import "package:expect/expect.dart";

class Server {
  late HttpServer server;

  Future<Server> start() async {
    server = await HttpServer.bind(InternetAddress.loopbackIPv4.address, 0);
    server.listen((request) {
      final response = request.response;

      // WARNING: this authenticate header is malformed because of missing
      // commas between the arguments
      if (request.uri.path == "/malformedAuthenticate") {
        response.statusCode = HttpStatus.unauthorized;
        response.headers.set(
          HttpHeaders.wwwAuthenticateHeader,
          "Bearer realm=\"realm\" error=\"invalid_token\"",
        );
        response.close();
        return;
      }

      // NOTE: see RFC6750 section 3 regarding the authenticate response header
      // field:
      // https://www.rfc-editor.org/rfc/rfc6750.html#section-3
      if (request.headers[HttpHeaders.authorizationHeader] != null) {
        final token = base64.encode(utf8.encode(request.uri.path.substring(1)));
        Expect.equals(
          1,
          request.headers[HttpHeaders.authorizationHeader]!.length,
        );
        final authorizationHeaderParts = request
            .headers[HttpHeaders.authorizationHeader]![0]
            .split(" ");
        Expect.equals("Bearer", authorizationHeaderParts[0]);
        if (token != authorizationHeaderParts[1]) {
          response.statusCode = HttpStatus.unauthorized;
          response.headers.set(
            HttpHeaders.wwwAuthenticateHeader,
            "Bearer realm=\"realm\", error=\"invalid_token\"",
          );
        }
      } else {
        response.statusCode = HttpStatus.unauthorized;
        response.headers.set(
          HttpHeaders.wwwAuthenticateHeader,
          "Bearer realm=\"realm\"",
        );
      }
      response.close();
    });
    return this;
  }

  Future<void> shutdown() => server.close();

  String get host => server.address.address;

  int get port => server.port;
}

void testCreateValidBearerTokens() {
  HttpClientBearerCredentials("977ce44bc56dc5000c9d2c329e682547");
  HttpClientBearerCredentials("dGVzdHRlc3R0ZXN0dGVzdA==");
  HttpClientBearerCredentials("mF_9.B5f-4.1JqM");
}

void testCreateInvalidBearerTokens() {
  Expect.throws(() => HttpClientBearerCredentials("#(&%)"));
  Expect.throws(() => HttpClientBearerCredentials("áéîöü"));
  Expect.throws(() => HttpClientBearerCredentials("あいうえお"));
  Expect.throws(() => HttpClientBearerCredentials("     "));
}

Future<void> testBearerWithoutCredentials() async {
  final server = await Server().start();
  final client = HttpClient();

  Future makeRequest(Uri url) async {
    final request = await client.getUrl(url);
    final response = await request.close();
    Expect.equals(HttpStatus.unauthorized, response.statusCode);
    return response.drain();
  }

  await Future.wait([
    for (int i = 0; i < 5; i++) ...[
      makeRequest(Uri.parse("http://${server.host}:${server.port}/test$i")),
    ],
  ]);

  await server.shutdown();
  client.close();
}

Future<void> testBearerWithCredentials() async {
  final server = await Server().start();
  final client = HttpClient();

  Future makeRequest(Uri url) async {
    final request = await client.getUrl(url);
    final response = await request.close();
    Expect.equals(HttpStatus.ok, response.statusCode);
    return response.drain();
  }

  for (int i = 0; i < 5; i++) {
    final token = base64.encode(utf8.encode("test$i"));
    client.addCredentials(
      Uri.parse("http://${server.host}:${server.port}/test$i"),
      "realm",
      HttpClientBearerCredentials(token),
    );
  }

  await Future.wait([
    for (int i = 0; i < 5; i++) ...[
      makeRequest(Uri.parse("http://${server.host}:${server.port}/test$i")),
    ],
  ]);

  await server.shutdown();
  client.close();
}

Future<void> testBearerWithAuthenticateCallback() async {
  final server = await Server().start();
  final client = HttpClient();

  final callbacks = <String>{};

  client.authenticate = (url, scheme, realm) async {
    Expect.equals("Bearer", scheme);
    Expect.equals("realm", realm);
    callbacks.add(url.path.substring(1));
    String token = base64.encode(utf8.encode(url.path.substring(1)));
    client.addCredentials(url, realm!, HttpClientBearerCredentials(token));
    return true;
  };

  Future makeRequest(Uri url) async {
    final request = await client.getUrl(url);
    final response = await request.close();
    Expect.equals(HttpStatus.ok, response.statusCode);
    return response.drain();
  }

  await Future.wait([
    for (int i = 0; i < 5; i++) ...[
      makeRequest(Uri.parse("http://${server.host}:${server.port}/test$i")),
    ],
  ]);

  // assert that all authenticate callbacks have actually been called
  Expect.setEquals({for (int i = 0; i < 5; i++) "test$i"}, callbacks);

  await server.shutdown();
  client.close();
}

Future<void> testMalformedAuthenticateHeaderWithoutCredentials() async {
  final server = await Server().start();
  final client = HttpClient();
  final uri = Uri.parse(
    "http://${server.host}:${server.port}/malformedAuthenticate",
  );

  // the request should resolve normally if no authentication is configured
  final request = await client.getUrl(uri);
  await request.close();

  await server.shutdown();
  client.close();
}

Future<void> testMalformedAuthenticateHeaderWithCredentials() async {
  final server = await Server().start();
  final client = HttpClient();
  final uri = Uri.parse(
    "http://${server.host}:${server.port}/malformedAuthenticate",
  );
  final token = base64.encode(utf8.encode("test"));

  // the request should throw an exception if credentials have been added
  client.addCredentials(uri, "realm", HttpClientBearerCredentials(token));
  await asyncExpectThrows<HttpException>(
    Future(() async {
      final request = await client.getUrl(uri);
      await request.close();
    }),
  );

  await server.shutdown();
  client.close();
}

Future<void> testMalformedAuthenticateHeaderWithAuthenticateCallback() async {
  final server = await Server().start();
  final client = HttpClient();
  final uri = Uri.parse(
    "http://${server.host}:${server.port}/malformedAuthenticate",
  );

  // the request should throw an exception if the authenticate handler is set
  client.authenticate = (url, scheme, realm) async => false;
  await asyncExpectThrows<HttpException>(
    Future(() async {
      final request = await client.getUrl(uri);
      await request.close();
    }),
  );

  await server.shutdown();
  client.close();
}

Future<void> testLocalServerBearer() async {
  final client = HttpClient();

  client.authenticate = (url, scheme, realm) async {
    final token = base64.encode(utf8.encode("test"));
    client.addCredentials(
      Uri.parse("http://127.0.0.1/bearer"),
      "test",
      HttpClientBearerCredentials(token),
    );
    return true;
  };

  final request = await client.getUrl(
    Uri.parse("http://127.0.0.1/bearer/test"),
  );
  final response = await request.close();
  Expect.equals(HttpStatus.ok, response.statusCode);
  await response.drain();

  client.close();
}

Future<void> main() async {
  asyncStart();
  testCreateValidBearerTokens();
  testCreateInvalidBearerTokens();
  await testBearerWithoutCredentials();
  await testBearerWithCredentials();
  await testBearerWithAuthenticateCallback();
  await testMalformedAuthenticateHeaderWithoutCredentials();
  await testMalformedAuthenticateHeaderWithCredentials();
  await testMalformedAuthenticateHeaderWithAuthenticateCallback();
  // These tests are not normally run. They can be used for locally
  // testing with another web server (e.g. Apache).
  // await testLocalServerBearer();
  asyncEnd();
}
